/*
 * Copyright (c) 2002-2003 by OpenSymphony
 * All rights reserved.
 */
package com.opensymphony.module.random;


/* ====================================================================
 * The OpenSymphony Software License, Version 1.1
 *
 * (this license is derived and fully compatible with the Apache Software
 * License - see http://www.apache.org/LICENSE.txt)
 *
 * Copyright (c) 2001 The OpenSymphony Group. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by the
 *        OpenSymphony Group (http://www.opensymphony.com/)."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. The names "OpenSymphony" and "The OpenSymphony Group"
 *    must not be used to endorse or promote products derived from this
 *    software without prior written permission. For written
 *    permission, please contact license@opensymphony.com .
 *
 * 5. Products derived from this software may not be called "OpenSymphony"
 *    or "OSCore", nor may "OpenSymphony" or "OSCore" appear in their
 *    name, without prior written permission of the OpenSymphony Group.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 */
import java.io.PrintWriter;

import java.security.InvalidKeyException;


/**
 * DOCUMENT ME!
 *
 * @author $author$
 * @version $Revision: 131 $
 */
public final class Rijndael_Algorithm // implicit no-argument constructor
 {
    //~ Static fields/initializers /////////////////////////////////////////////

    static final String NAME = "Rijndael_Algorithm";
    static final boolean IN = true;
    static final boolean OUT = false;
    static final boolean DEBUG = Rijndael_Properties.GLOBAL_DEBUG;
    static final int debuglevel = DEBUG ? Rijndael_Properties.getLevel(NAME) : 0;
    static final PrintWriter err = DEBUG ? Rijndael_Properties.getOutput() : null;
    static final boolean TRACE = Rijndael_Properties.isTraceable(NAME);

    // Constants and variables
    //...........................................................................
    static final int BLOCK_SIZE = 16; // default block size in bytes
    static final int[] alog = new int[256];
    static final int[] log = new int[256];
    static final byte[] S = new byte[256];
    static final byte[] Si = new byte[256];
    static final int[] T1 = new int[256];
    static final int[] T2 = new int[256];
    static final int[] T3 = new int[256];
    static final int[] T4 = new int[256];
    static final int[] T5 = new int[256];
    static final int[] T6 = new int[256];
    static final int[] T7 = new int[256];
    static final int[] T8 = new int[256];
    static final int[] U1 = new int[256];
    static final int[] U2 = new int[256];
    static final int[] U3 = new int[256];
    static final int[] U4 = new int[256];
    static final byte[] rcon = new byte[30];
    static final int[][][] shifts = new int[][][] {
        {
            {0, 0},
            {1, 3},
            {2, 2},
            {3, 1}
        },
        {
            {0, 0},
            {1, 5},
            {2, 4},
            {3, 3}
        },
        {
            {0, 0},
            {1, 7},
            {3, 5},
            {4, 4}
        }
    };
    private static final char[] HEX_DIGITS = {
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D',
        'E', 'F'
    };

    // Static code - to intialise S-boxes and T-boxes
    //...........................................................................
    static {
        long time = System.currentTimeMillis();

        if (DEBUG && (debuglevel > 6)) {
            System.out.println("Algorithm Name: " + Rijndael_Properties.FULL_NAME);
            System.out.println("Electronic Codebook (ECB) Mode");
            System.out.println();
        }

        int ROOT = 0x11B;
        int i;
        int j = 0;

        //
        // produce log and alog tables, needed for multiplying in the
        // field GF(2^m) (generator = 3)
        //
        alog[0] = 1;

        for (i = 1; i < 256; i++) {
            j = (alog[i - 1] << 1) ^ alog[i - 1];

            if ((j & 0x100) != 0) {
                j ^= ROOT;
            }

            alog[i] = j;
        }

        for (i = 1; i < 255; i++) {
            log[alog[i]] = i;
        }

        byte[][] A = new byte[][] {
            {1, 1, 1, 1, 1, 0, 0, 0},
            {0, 1, 1, 1, 1, 1, 0, 0},
            {0, 0, 1, 1, 1, 1, 1, 0},
            {0, 0, 0, 1, 1, 1, 1, 1},
            {1, 0, 0, 0, 1, 1, 1, 1},
            {1, 1, 0, 0, 0, 1, 1, 1},
            {1, 1, 1, 0, 0, 0, 1, 1},
            {1, 1, 1, 1, 0, 0, 0, 1}
        };
        byte[] B = new byte[] {0, 1, 1, 0, 0, 0, 1, 1};

        //
        // substitution box based on F^{-1}(x)
        //
        int t;
        byte[][] box = new byte[256][8];
        box[1][7] = 1;

        for (i = 2; i < 256; i++) {
            j = alog[255 - log[i]];

            for (t = 0; t < 8; t++) {
                box[i][t] = (byte) ((j >>> (7 - t)) & 0x01);
            }
        }

        //
        // affine transform:  box[i] <- B + A*box[i]
        //
        byte[][] cox = new byte[256][8];

        for (i = 0; i < 256; i++) {
            for (t = 0; t < 8; t++) {
                cox[i][t] = B[t];

                for (j = 0; j < 8; j++) {
                    cox[i][t] ^= (A[t][j] * box[i][j]);
                }
            }
        }

        //
        // S-boxes and inverse S-boxes
        //
        for (i = 0; i < 256; i++) {
            S[i] = (byte) (cox[i][0] << 7);

            for (t = 1; t < 8; t++) {
                S[i] ^= (cox[i][t] << (7 - t));
            }

            Si[S[i] & 0xFF] = (byte) i;
        }

        //
        // T-boxes
        //
        byte[][] G = new byte[][] {
            {2, 1, 1, 3},
            {3, 2, 1, 1},
            {1, 3, 2, 1},
            {1, 1, 3, 2}
        };
        byte[][] AA = new byte[4][8];

        for (i = 0; i < 4; i++) {
            for (j = 0; j < 4; j++) {
                AA[i][j] = G[i][j];
            }

            AA[i][i + 4] = 1;
        }

        byte pivot;
        byte tmp;
        byte[][] iG = new byte[4][4];

        for (i = 0; i < 4; i++) {
            pivot = AA[i][i];

            if (pivot == 0) {
                t = i + 1;

                while ((AA[t][i] == 0) && (t < 4)) {
                    t++;
                }

                if (t == 4) {
                    throw new RuntimeException("G matrix is not invertible");
                } else {
                    for (j = 0; j < 8; j++) {
                        tmp = AA[i][j];
                        AA[i][j] = AA[t][j];
                        AA[t][j] = (byte) tmp;
                    }

                    pivot = AA[i][i];
                }
            }

            for (j = 0; j < 8; j++) {
                if (AA[i][j] != 0) {
                    AA[i][j] = (byte) alog[((255 + log[AA[i][j] & 0xFF]) - log[pivot & 0xFF]) % 255];
                }
            }

            for (t = 0; t < 4; t++) {
                if (i != t) {
                    for (j = i + 1; j < 8; j++) {
                        AA[t][j] ^= mul(AA[i][j], AA[t][i]);
                    }

                    AA[t][i] = 0;
                }
            }
        }

        for (i = 0; i < 4; i++) {
            for (j = 0; j < 4; j++) {
                iG[i][j] = AA[i][j + 4];
            }
        }

        int s;

        for (t = 0; t < 256; t++) {
            s = S[t];
            T1[t] = mul4(s, G[0]);
            T2[t] = mul4(s, G[1]);
            T3[t] = mul4(s, G[2]);
            T4[t] = mul4(s, G[3]);

            s = Si[t];
            T5[t] = mul4(s, iG[0]);
            T6[t] = mul4(s, iG[1]);
            T7[t] = mul4(s, iG[2]);
            T8[t] = mul4(s, iG[3]);

            U1[t] = mul4(t, iG[0]);
            U2[t] = mul4(t, iG[1]);
            U3[t] = mul4(t, iG[2]);
            U4[t] = mul4(t, iG[3]);
        }

        //
        // round constants
        //
        rcon[0] = 1;

        int r = 1;

        for (t = 1; t < 30;) {
            rcon[t++] = (byte) (r = mul(2, r));
        }

        time = System.currentTimeMillis() - time;

        if (DEBUG && (debuglevel > 8)) {
            System.out.println("==========");
            System.out.println();
            System.out.println("Static Data");
            System.out.println();
            System.out.println("S[]:");

            for (i = 0; i < 16; i++) {
                for (j = 0; j < 16; j++) {
                    System.out.print("0x" + byteToString(S[(i * 16) + j]) + ", ");
                }

                System.out.println();
            }

            System.out.println();
            System.out.println("Si[]:");

            for (i = 0; i < 16; i++) {
                for (j = 0; j < 16; j++) {
                    System.out.print("0x" + byteToString(Si[(i * 16) + j]) + ", ");
                }

                System.out.println();
            }

            System.out.println();
            System.out.println("iG[]:");

            for (i = 0; i < 4; i++) {
                for (j = 0; j < 4; j++) {
                    System.out.print("0x" + byteToString(iG[i][j]) + ", ");
                }

                System.out.println();
            }

            System.out.println();
            System.out.println("T1[]:");

            for (i = 0; i < 64; i++) {
                for (j = 0; j < 4; j++) {
                    System.out.print("0x" + intToString(T1[(i * 4) + j]) + ", ");
                }

                System.out.println();
            }

            System.out.println();
            System.out.println("T2[]:");

            for (i = 0; i < 64; i++) {
                for (j = 0; j < 4; j++) {
                    System.out.print("0x" + intToString(T2[(i * 4) + j]) + ", ");
                }

                System.out.println();
            }

            System.out.println();
            System.out.println("T3[]:");

            for (i = 0; i < 64; i++) {
                for (j = 0; j < 4; j++) {
                    System.out.print("0x" + intToString(T3[(i * 4) + j]) + ", ");
                }

                System.out.println();
            }

            System.out.println();
            System.out.println("T4[]:");

            for (i = 0; i < 64; i++) {
                for (j = 0; j < 4; j++) {
                    System.out.print("0x" + intToString(T4[(i * 4) + j]) + ", ");
                }

                System.out.println();
            }

            System.out.println();
            System.out.println("T5[]:");

            for (i = 0; i < 64; i++) {
                for (j = 0; j < 4; j++) {
                    System.out.print("0x" + intToString(T5[(i * 4) + j]) + ", ");
                }

                System.out.println();
            }

            System.out.println();
            System.out.println("T6[]:");

            for (i = 0; i < 64; i++) {
                for (j = 0; j < 4; j++) {
                    System.out.print("0x" + intToString(T6[(i * 4) + j]) + ", ");
                }

                System.out.println();
            }

            System.out.println();
            System.out.println("T7[]:");

            for (i = 0; i < 64; i++) {
                for (j = 0; j < 4; j++) {
                    System.out.print("0x" + intToString(T7[(i * 4) + j]) + ", ");
                }

                System.out.println();
            }

            System.out.println();
            System.out.println("T8[]:");

            for (i = 0; i < 64; i++) {
                for (j = 0; j < 4; j++) {
                    System.out.print("0x" + intToString(T8[(i * 4) + j]) + ", ");
                }

                System.out.println();
            }

            System.out.println();
            System.out.println("U1[]:");

            for (i = 0; i < 64; i++) {
                for (j = 0; j < 4; j++) {
                    System.out.print("0x" + intToString(U1[(i * 4) + j]) + ", ");
                }

                System.out.println();
            }

            System.out.println();
            System.out.println("U2[]:");

            for (i = 0; i < 64; i++) {
                for (j = 0; j < 4; j++) {
                    System.out.print("0x" + intToString(U2[(i * 4) + j]) + ", ");
                }

                System.out.println();
            }

            System.out.println();
            System.out.println("U3[]:");

            for (i = 0; i < 64; i++) {
                for (j = 0; j < 4; j++) {
                    System.out.print("0x" + intToString(U3[(i * 4) + j]) + ", ");
                }

                System.out.println();
            }

            System.out.println();
            System.out.println("U4[]:");

            for (i = 0; i < 64; i++) {
                for (j = 0; j < 4; j++) {
                    System.out.print("0x" + intToString(U4[(i * 4) + j]) + ", ");
                }

                System.out.println();
            }

            System.out.println();
            System.out.println("rcon[]:");

            for (i = 0; i < 5; i++) {
                for (j = 0; j < 6; j++) {
                    System.out.print("0x" + byteToString(rcon[(i * 6) + j]) + ", ");
                }

                System.out.println();
            }

            System.out.println();
            System.out.println("Total initialization time: " + time + " ms.");
            System.out.println();
        }
    }

    //~ Methods ////////////////////////////////////////////////////////////////

    /**
     * Return The number of rounds for a given Rijndael's key and block sizes.
     *
     * @param keySize    The size of the user key material in bytes.
     * @param blockSize  The desired block size in bytes.
     * @return The number of rounds for a given Rijndael's key and
     *      block sizes.
     */
    public static int getRounds(int keySize, int blockSize) {
        switch (keySize) {
        case 16:
            return (blockSize == 16) ? 10 : ((blockSize == 24) ? 12 : 14);

        case 24:
            return (blockSize != 32) ? 12 : 14;

        default: // 32 bytes = 256 bits

            return 14;
        }
    }

    /**
     * Convenience method to decrypt exactly one block of plaintext, assuming
     * Rijndael's default block size (128-bit).
     *
     * @param  in         The ciphertext.
     * @param  inOffset   Index of in from which to start considering data.
     * @param  sessionKey The session key to use for decryption.
     * @return The plaintext generated from a ciphertext using the session key.
     */
    public static byte[] blockDecrypt(byte[] in, int inOffset, Object sessionKey) {
        if (DEBUG) {
            trace(IN, "blockDecrypt(" + in + ", " + inOffset + ", " + sessionKey + ")");
        }

        int[][] Kd = (int[][]) ((Object[]) sessionKey)[1]; // extract decryption round keys
        int ROUNDS = Kd.length - 1;
        int[] Kdr = Kd[0];

        // ciphertext to ints + key
        int t0 = (((in[inOffset++] & 0xFF) << 24) | ((in[inOffset++] & 0xFF) << 16) | ((in[inOffset++] & 0xFF) << 8) | (in[inOffset++] & 0xFF)) ^ Kdr[0];
        int t1 = (((in[inOffset++] & 0xFF) << 24) | ((in[inOffset++] & 0xFF) << 16) | ((in[inOffset++] & 0xFF) << 8) | (in[inOffset++] & 0xFF)) ^ Kdr[1];
        int t2 = (((in[inOffset++] & 0xFF) << 24) | ((in[inOffset++] & 0xFF) << 16) | ((in[inOffset++] & 0xFF) << 8) | (in[inOffset++] & 0xFF)) ^ Kdr[2];
        int t3 = (((in[inOffset++] & 0xFF) << 24) | ((in[inOffset++] & 0xFF) << 16) | ((in[inOffset++] & 0xFF) << 8) | (in[inOffset++] & 0xFF)) ^ Kdr[3];

        int a0;
        int a1;
        int a2;
        int a3;

        for (int r = 1; r < ROUNDS; r++) { // apply round transforms
            Kdr = Kd[r];
            a0 = (T5[(t0 >>> 24) & 0xFF] ^ T6[(t3 >>> 16) & 0xFF] ^ T7[(t2 >>> 8) & 0xFF] ^ T8[t1 & 0xFF]) ^ Kdr[0];
            a1 = (T5[(t1 >>> 24) & 0xFF] ^ T6[(t0 >>> 16) & 0xFF] ^ T7[(t3 >>> 8) & 0xFF] ^ T8[t2 & 0xFF]) ^ Kdr[1];
            a2 = (T5[(t2 >>> 24) & 0xFF] ^ T6[(t1 >>> 16) & 0xFF] ^ T7[(t0 >>> 8) & 0xFF] ^ T8[t3 & 0xFF]) ^ Kdr[2];
            a3 = (T5[(t3 >>> 24) & 0xFF] ^ T6[(t2 >>> 16) & 0xFF] ^ T7[(t1 >>> 8) & 0xFF] ^ T8[t0 & 0xFF]) ^ Kdr[3];
            t0 = a0;
            t1 = a1;
            t2 = a2;
            t3 = a3;

            if (DEBUG && (debuglevel > 6)) {
                System.out.println("PT" + r + "=" + intToString(t0) + intToString(t1) + intToString(t2) + intToString(t3));
            }
        }

        // last round is special
        byte[] result = new byte[16]; // the resulting plaintext
        Kdr = Kd[ROUNDS];

        int tt = Kdr[0];
        result[0] = (byte) (Si[(t0 >>> 24) & 0xFF] ^ (tt >>> 24));
        result[1] = (byte) (Si[(t3 >>> 16) & 0xFF] ^ (tt >>> 16));
        result[2] = (byte) (Si[(t2 >>> 8) & 0xFF] ^ (tt >>> 8));
        result[3] = (byte) (Si[t1 & 0xFF] ^ tt);
        tt = Kdr[1];
        result[4] = (byte) (Si[(t1 >>> 24) & 0xFF] ^ (tt >>> 24));
        result[5] = (byte) (Si[(t0 >>> 16) & 0xFF] ^ (tt >>> 16));
        result[6] = (byte) (Si[(t3 >>> 8) & 0xFF] ^ (tt >>> 8));
        result[7] = (byte) (Si[t2 & 0xFF] ^ tt);
        tt = Kdr[2];
        result[8] = (byte) (Si[(t2 >>> 24) & 0xFF] ^ (tt >>> 24));
        result[9] = (byte) (Si[(t1 >>> 16) & 0xFF] ^ (tt >>> 16));
        result[10] = (byte) (Si[(t0 >>> 8) & 0xFF] ^ (tt >>> 8));
        result[11] = (byte) (Si[t3 & 0xFF] ^ tt);
        tt = Kdr[3];
        result[12] = (byte) (Si[(t3 >>> 24) & 0xFF] ^ (tt >>> 24));
        result[13] = (byte) (Si[(t2 >>> 16) & 0xFF] ^ (tt >>> 16));
        result[14] = (byte) (Si[(t1 >>> 8) & 0xFF] ^ (tt >>> 8));
        result[15] = (byte) (Si[t0 & 0xFF] ^ tt);

        if (DEBUG && (debuglevel > 6)) {
            System.out.println("PT=" + toString(result));
            System.out.println();
        }

        if (DEBUG) {
            trace(OUT, "blockDecrypt()");
        }

        return result;
    }

    /**
     * Decrypt exactly one block of ciphertext.
     *
     * @param  in         The ciphertext.
     * @param  inOffset   Index of in from which to start considering data.
     * @param  sessionKey The session key to use for decryption.
     * @param  blockSize  The block size in bytes of this Rijndael.
     * @return The plaintext generated from a ciphertext using the session key.
     */
    public static byte[] blockDecrypt(byte[] in, int inOffset, Object sessionKey, int blockSize) {
        if (blockSize == BLOCK_SIZE) {
            return blockDecrypt(in, inOffset, sessionKey);
        }

        if (DEBUG) {
            trace(IN, "blockDecrypt(" + in + ", " + inOffset + ", " + sessionKey + ", " + blockSize + ")");
        }

        Object[] sKey = (Object[]) sessionKey; // extract decryption round keys
        int[][] Kd = (int[][]) sKey[1];

        int BC = blockSize / 4;
        int ROUNDS = Kd.length - 1;
        int SC = (BC == 4) ? 0 : ((BC == 6) ? 1 : 2);
        int s1 = shifts[SC][1][1];
        int s2 = shifts[SC][2][1];
        int s3 = shifts[SC][3][1];
        int[] a = new int[BC];
        int[] t = new int[BC]; // temporary work array
        int i;
        byte[] result = new byte[blockSize]; // the resulting plaintext
        int j = 0;
        int tt;

        for (i = 0; i < BC; i++) // ciphertext to ints + key
         {
            t[i] = (((in[inOffset++] & 0xFF) << 24) | ((in[inOffset++] & 0xFF) << 16) | ((in[inOffset++] & 0xFF) << 8) | (in[inOffset++] & 0xFF)) ^ Kd[0][i];
        }

        for (int r = 1; r < ROUNDS; r++) { // apply round transforms

            for (i = 0; i < BC; i++) {
                a[i] = (T5[(t[i] >>> 24) & 0xFF] ^ T6[(t[(i + s1) % BC] >>> 16) & 0xFF] ^ T7[(t[(i + s2) % BC] >>> 8) & 0xFF] ^ T8[t[(i + s3) % BC] & 0xFF]) ^ Kd[r][i];
            }

            System.arraycopy(a, 0, t, 0, BC);

            if (DEBUG && (debuglevel > 6)) {
                System.out.println("PT" + r + "=" + toString(t));
            }
        }

        for (i = 0; i < BC; i++) { // last round is special
            tt = Kd[ROUNDS][i];
            result[j++] = (byte) (Si[(t[i] >>> 24) & 0xFF] ^ (tt >>> 24));
            result[j++] = (byte) (Si[(t[(i + s1) % BC] >>> 16) & 0xFF] ^ (tt >>> 16));
            result[j++] = (byte) (Si[(t[(i + s2) % BC] >>> 8) & 0xFF] ^ (tt >>> 8));
            result[j++] = (byte) (Si[t[(i + s3) % BC] & 0xFF] ^ tt);
        }

        if (DEBUG && (debuglevel > 6)) {
            System.out.println("PT=" + toString(result));
            System.out.println();
        }

        if (DEBUG) {
            trace(OUT, "blockDecrypt()");
        }

        return result;
    }

    /**
     * Convenience method to encrypt exactly one block of plaintext, assuming
     * Rijndael's default block size (128-bit).
     *
     * @param  in         The plaintext.
     * @param  inOffset   Index of in from which to start considering data.
     * @param  sessionKey The session key to use for encryption.
     * @return The ciphertext generated from a plaintext using the session key.
     */
    public static byte[] blockEncrypt(byte[] in, int inOffset, Object sessionKey) {
        if (DEBUG) {
            trace(IN, "blockEncrypt(" + in + ", " + inOffset + ", " + sessionKey + ")");
        }

        int[][] Ke = (int[][]) ((Object[]) sessionKey)[0]; // extract encryption round keys
        int ROUNDS = Ke.length - 1;
        int[] Ker = Ke[0];

        // plaintext to ints + key
        int t0 = (((in[inOffset++] & 0xFF) << 24) | ((in[inOffset++] & 0xFF) << 16) | ((in[inOffset++] & 0xFF) << 8) | (in[inOffset++] & 0xFF)) ^ Ker[0];
        int t1 = (((in[inOffset++] & 0xFF) << 24) | ((in[inOffset++] & 0xFF) << 16) | ((in[inOffset++] & 0xFF) << 8) | (in[inOffset++] & 0xFF)) ^ Ker[1];
        int t2 = (((in[inOffset++] & 0xFF) << 24) | ((in[inOffset++] & 0xFF) << 16) | ((in[inOffset++] & 0xFF) << 8) | (in[inOffset++] & 0xFF)) ^ Ker[2];
        int t3 = (((in[inOffset++] & 0xFF) << 24) | ((in[inOffset++] & 0xFF) << 16) | ((in[inOffset++] & 0xFF) << 8) | (in[inOffset++] & 0xFF)) ^ Ker[3];

        int a0;
        int a1;
        int a2;
        int a3;

        for (int r = 1; r < ROUNDS; r++) { // apply round transforms
            Ker = Ke[r];
            a0 = (T1[(t0 >>> 24) & 0xFF] ^ T2[(t1 >>> 16) & 0xFF] ^ T3[(t2 >>> 8) & 0xFF] ^ T4[t3 & 0xFF]) ^ Ker[0];
            a1 = (T1[(t1 >>> 24) & 0xFF] ^ T2[(t2 >>> 16) & 0xFF] ^ T3[(t3 >>> 8) & 0xFF] ^ T4[t0 & 0xFF]) ^ Ker[1];
            a2 = (T1[(t2 >>> 24) & 0xFF] ^ T2[(t3 >>> 16) & 0xFF] ^ T3[(t0 >>> 8) & 0xFF] ^ T4[t1 & 0xFF]) ^ Ker[2];
            a3 = (T1[(t3 >>> 24) & 0xFF] ^ T2[(t0 >>> 16) & 0xFF] ^ T3[(t1 >>> 8) & 0xFF] ^ T4[t2 & 0xFF]) ^ Ker[3];
            t0 = a0;
            t1 = a1;
            t2 = a2;
            t3 = a3;

            if (DEBUG && (debuglevel > 6)) {
                System.out.println("CT" + r + "=" + intToString(t0) + intToString(t1) + intToString(t2) + intToString(t3));
            }
        }

        // last round is special
        byte[] result = new byte[BLOCK_SIZE]; // the resulting ciphertext
        Ker = Ke[ROUNDS];

        int tt = Ker[0];
        result[0] = (byte) (S[(t0 >>> 24) & 0xFF] ^ (tt >>> 24));
        result[1] = (byte) (S[(t1 >>> 16) & 0xFF] ^ (tt >>> 16));
        result[2] = (byte) (S[(t2 >>> 8) & 0xFF] ^ (tt >>> 8));
        result[3] = (byte) (S[t3 & 0xFF] ^ tt);
        tt = Ker[1];
        result[4] = (byte) (S[(t1 >>> 24) & 0xFF] ^ (tt >>> 24));
        result[5] = (byte) (S[(t2 >>> 16) & 0xFF] ^ (tt >>> 16));
        result[6] = (byte) (S[(t3 >>> 8) & 0xFF] ^ (tt >>> 8));
        result[7] = (byte) (S[t0 & 0xFF] ^ tt);
        tt = Ker[2];
        result[8] = (byte) (S[(t2 >>> 24) & 0xFF] ^ (tt >>> 24));
        result[9] = (byte) (S[(t3 >>> 16) & 0xFF] ^ (tt >>> 16));
        result[10] = (byte) (S[(t0 >>> 8) & 0xFF] ^ (tt >>> 8));
        result[11] = (byte) (S[t1 & 0xFF] ^ tt);
        tt = Ker[3];
        result[12] = (byte) (S[(t3 >>> 24) & 0xFF] ^ (tt >>> 24));
        result[13] = (byte) (S[(t0 >>> 16) & 0xFF] ^ (tt >>> 16));
        result[14] = (byte) (S[(t1 >>> 8) & 0xFF] ^ (tt >>> 8));
        result[15] = (byte) (S[t2 & 0xFF] ^ tt);

        if (DEBUG && (debuglevel > 6)) {
            System.out.println("CT=" + toString(result));
            System.out.println();
        }

        if (DEBUG) {
            trace(OUT, "blockEncrypt()");
        }

        return result;
    }

    /**
     * Encrypt exactly one block of plaintext.
     *
     * @param  in         The plaintext.
     * @param  inOffset   Index of in from which to start considering data.
     * @param  sessionKey The session key to use for encryption.
     * @param  blockSize  The block size in bytes of this Rijndael.
     * @return The ciphertext generated from a plaintext using the session key.
     */
    public static byte[] blockEncrypt(byte[] in, int inOffset, Object sessionKey, int blockSize) {
        if (blockSize == BLOCK_SIZE) {
            return blockEncrypt(in, inOffset, sessionKey);
        }

        if (DEBUG) {
            trace(IN, "blockEncrypt(" + in + ", " + inOffset + ", " + sessionKey + ", " + blockSize + ")");
        }

        Object[] sKey = (Object[]) sessionKey; // extract encryption round keys
        int[][] Ke = (int[][]) sKey[0];

        int BC = blockSize / 4;
        int ROUNDS = Ke.length - 1;
        int SC = (BC == 4) ? 0 : ((BC == 6) ? 1 : 2);
        int s1 = shifts[SC][1][0];
        int s2 = shifts[SC][2][0];
        int s3 = shifts[SC][3][0];
        int[] a = new int[BC];
        int[] t = new int[BC]; // temporary work array
        int i;
        byte[] result = new byte[blockSize]; // the resulting ciphertext
        int j = 0;
        int tt;

        for (i = 0; i < BC; i++) // plaintext to ints + key
         {
            t[i] = (((in[inOffset++] & 0xFF) << 24) | ((in[inOffset++] & 0xFF) << 16) | ((in[inOffset++] & 0xFF) << 8) | (in[inOffset++] & 0xFF)) ^ Ke[0][i];
        }

        for (int r = 1; r < ROUNDS; r++) { // apply round transforms

            for (i = 0; i < BC; i++) {
                a[i] = (T1[(t[i] >>> 24) & 0xFF] ^ T2[(t[(i + s1) % BC] >>> 16) & 0xFF] ^ T3[(t[(i + s2) % BC] >>> 8) & 0xFF] ^ T4[t[(i + s3) % BC] & 0xFF]) ^ Ke[r][i];
            }

            System.arraycopy(a, 0, t, 0, BC);

            if (DEBUG && (debuglevel > 6)) {
                System.out.println("CT" + r + "=" + toString(t));
            }
        }

        for (i = 0; i < BC; i++) { // last round is special
            tt = Ke[ROUNDS][i];
            result[j++] = (byte) (S[(t[i] >>> 24) & 0xFF] ^ (tt >>> 24));
            result[j++] = (byte) (S[(t[(i + s1) % BC] >>> 16) & 0xFF] ^ (tt >>> 16));
            result[j++] = (byte) (S[(t[(i + s2) % BC] >>> 8) & 0xFF] ^ (tt >>> 8));
            result[j++] = (byte) (S[t[(i + s3) % BC] & 0xFF] ^ tt);
        }

        if (DEBUG && (debuglevel > 6)) {
            System.out.println("CT=" + toString(result));
            System.out.println();
        }

        if (DEBUG) {
            trace(OUT, "blockEncrypt()");
        }

        return result;
    }

    // Rijndael own methods
    //...........................................................................

    /** @return The default length in bytes of the Algorithm input block. */
    public static int blockSize() {
        return BLOCK_SIZE;
    }

    // main(): use to generate the Intermediate Values KAT
    //...........................................................................
    public static void main(String[] args) {
        self_test(16);
        self_test(24);
        self_test(32);
    }

    // Basic API methods
    //...........................................................................

    /**
     * Convenience method to expand a user-supplied key material into a
     * session key, assuming Rijndael's default block size (128-bit).
     *
     * @param k The 128/192/256-bit user-key to use.
     * @exception  InvalidKeyException  If the key is invalid.
     */
    public static Object makeKey(byte[] k) throws InvalidKeyException {
        return makeKey(k, BLOCK_SIZE);
    }

    /**
     * Expand a user-supplied key material into a session key.
     *
     * @param k        The 128/192/256-bit user-key to use.
     * @param blockSize  The block size in bytes of this Rijndael.
     * @exception  InvalidKeyException  If the key is invalid.
     */
    public static synchronized Object makeKey(byte[] k, int blockSize) throws InvalidKeyException {
        if (DEBUG) {
            trace(IN, "makeKey(" + k + ", " + blockSize + ")");
        }

        if (k == null) {
            throw new InvalidKeyException("Empty key");
        }

        if (!((k.length == 16) || (k.length == 24) || (k.length == 32))) {
            throw new InvalidKeyException("Incorrect key length");
        }

        int ROUNDS = getRounds(k.length, blockSize);
        int BC = blockSize / 4;
        int[][] Ke = new int[ROUNDS + 1][BC]; // encryption round keys
        int[][] Kd = new int[ROUNDS + 1][BC]; // decryption round keys
        int ROUND_KEY_COUNT = (ROUNDS + 1) * BC;
        int KC = k.length / 4;
        int[] tk = new int[KC];
        int i;
        int j;

        // copy user material bytes into temporary ints
        for (i = 0, j = 0; i < KC;) {
            tk[i++] = ((k[j++] & 0xFF) << 24) | ((k[j++] & 0xFF) << 16) | ((k[j++] & 0xFF) << 8) | (k[j++] & 0xFF);
        }

        // copy values into round key arrays
        int t = 0;

        for (j = 0; (j < KC) && (t < ROUND_KEY_COUNT); j++, t++) {
            Ke[t / BC][t % BC] = tk[j];
            Kd[ROUNDS - (t / BC)][t % BC] = tk[j];
        }

        int tt;
        int rconpointer = 0;

        while (t < ROUND_KEY_COUNT) {
            // extrapolate using phi (the round key evolution function)
            tt = tk[KC - 1];
            tk[0] ^= (((S[(tt >>> 16) & 0xFF] & 0xFF) << 24) ^ ((S[(tt >>> 8) & 0xFF] & 0xFF) << 16) ^ ((S[tt & 0xFF] & 0xFF) << 8) ^ (S[(tt >>> 24) & 0xFF] & 0xFF) ^ ((rcon[rconpointer++] & 0xFF) << 24));

            if (KC != 8) {
                for (i = 1, j = 0; i < KC;) {
                    tk[i++] ^= tk[j++];
                }
            } else {
                for (i = 1, j = 0; i < (KC / 2);) {
                    tk[i++] ^= tk[j++];
                }

                tt = tk[(KC / 2) - 1];
                tk[KC / 2] ^= ((S[tt & 0xFF] & 0xFF) ^ ((S[(tt >>> 8) & 0xFF] & 0xFF) << 8) ^ ((S[(tt >>> 16) & 0xFF] & 0xFF) << 16) ^ ((S[(tt >>> 24) & 0xFF] & 0xFF) << 24));

                for (j = KC / 2, i = j + 1; i < KC;) {
                    tk[i++] ^= tk[j++];
                }
            }

            // copy values into round key arrays
            for (j = 0; (j < KC) && (t < ROUND_KEY_COUNT); j++, t++) {
                Ke[t / BC][t % BC] = tk[j];
                Kd[ROUNDS - (t / BC)][t % BC] = tk[j];
            }
        }

        for (int r = 1; r < ROUNDS; r++) // inverse MixColumn where needed
         {
            for (j = 0; j < BC; j++) {
                tt = Kd[r][j];
                Kd[r][j] = U1[(tt >>> 24) & 0xFF] ^ U2[(tt >>> 16) & 0xFF] ^ U3[(tt >>> 8) & 0xFF] ^ U4[tt & 0xFF];
            }
        }

        // assemble the encryption (Ke) and decryption (Kd) round keys into
        // one sessionKey object
        Object[] sessionKey = new Object[] {Ke, Kd};

        if (DEBUG) {
            trace(OUT, "makeKey()");
        }

        return sessionKey;
    }

    /** A basic symmetric encryption/decryption test. */
    public static boolean self_test() {
        return self_test(BLOCK_SIZE);
    }

    static void debug(String s) {
        err.println(">>> " + NAME + ": " + s);
    }

    // multiply two elements of GF(2^m)
    static final int mul(int a, int b) {
        return ((a != 0) && (b != 0)) ? alog[(log[a & 0xFF] + log[b & 0xFF]) % 255] : 0;
    }

    // convenience method used in generating Transposition boxes
    static final int mul4(int a, byte[] b) {
        if (a == 0) {
            return 0;
        }

        a = log[a & 0xFF];

        int a0 = (b[0] != 0) ? (alog[(a + log[b[0] & 0xFF]) % 255] & 0xFF) : 0;
        int a1 = (b[1] != 0) ? (alog[(a + log[b[1] & 0xFF]) % 255] & 0xFF) : 0;
        int a2 = (b[2] != 0) ? (alog[(a + log[b[2] & 0xFF]) % 255] & 0xFF) : 0;
        int a3 = (b[3] != 0) ? (alog[(a + log[b[3] & 0xFF]) % 255] & 0xFF) : 0;

        return (a0 << 24) | (a1 << 16) | (a2 << 8) | a3;
    }

    static void trace(boolean in, String s) {
        if (TRACE) {
            err.println((in ? "==> " : "<== ") + NAME + "." + s);
        }
    }

    static void trace(String s) {
        if (TRACE) {
            err.println("<=> " + NAME + "." + s);
        }
    }

    // utility static methods (from cryptix.util.core ArrayUtil and Hex classes)
    //...........................................................................

    /**
     * Compares two byte arrays for equality.
     *
     * @return true if the arrays have identical contents
     */
    private static boolean areEqual(byte[] a, byte[] b) {
        int aLength = a.length;

        if (aLength != b.length) {
            return false;
        }

        for (int i = 0; i < aLength; i++) {
            if (a[i] != b[i]) {
                return false;
            }
        }

        return true;
    }

    /**
     * Returns a string of 2 hexadecimal digits (most significant
     * digit first) corresponding to the lowest 8 bits of <i>n</i>.
     */
    private static String byteToString(int n) {
        char[] buf = {HEX_DIGITS[(n >>> 4) & 0x0F], HEX_DIGITS[n & 0x0F]};

        return new String(buf);
    }

    /**
     * Returns a string of 8 hexadecimal digits (most significant
     * digit first) corresponding to the integer <i>n</i>, which is
     * treated as unsigned.
     */
    private static String intToString(int n) {
        char[] buf = new char[8];

        for (int i = 7; i >= 0; i--) {
            buf[i] = HEX_DIGITS[n & 0x0F];
            n >>>= 4;
        }

        return new String(buf);
    }

    /** A basic symmetric encryption/decryption test for a given key size. */
    private static boolean self_test(int keysize) {
        if (DEBUG) {
            trace(IN, "self_test(" + keysize + ")");
        }

        boolean ok = false;

        try {
            byte[] kb = new byte[keysize];
            byte[] pt = new byte[BLOCK_SIZE];
            int i;

            for (i = 0; i < keysize; i++) {
                kb[i] = (byte) i;
            }

            for (i = 0; i < BLOCK_SIZE; i++) {
                pt[i] = (byte) i;
            }

            if (DEBUG && (debuglevel > 6)) {
                System.out.println("==========");
                System.out.println();
                System.out.println("KEYSIZE=" + (8 * keysize));
                System.out.println("KEY=" + toString(kb));
                System.out.println();
            }

            Object key = makeKey(kb, BLOCK_SIZE);

            if (DEBUG && (debuglevel > 6)) {
                System.out.println("Intermediate Ciphertext Values (Encryption)");
                System.out.println();
                System.out.println("PT=" + toString(pt));
            }

            byte[] ct = blockEncrypt(pt, 0, key, BLOCK_SIZE);

            if (DEBUG && (debuglevel > 6)) {
                System.out.println("Intermediate Plaintext Values (Decryption)");
                System.out.println();
                System.out.println("CT=" + toString(ct));
            }

            byte[] cpt = blockDecrypt(ct, 0, key, BLOCK_SIZE);

            ok = areEqual(pt, cpt);

            if (!ok) {
                throw new RuntimeException("Symmetric operation failed");
            }
        } catch (Exception x) {
            if (DEBUG && (debuglevel > 0)) {
                debug("Exception encountered during self-test: " + x.getMessage());
                x.printStackTrace();
            }
        }

        if (DEBUG && (debuglevel > 0)) {
            debug("Self-test OK? " + ok);
        }

        if (DEBUG) {
            trace(OUT, "self_test()");
        }

        return ok;
    }

    /**
     * Returns a string of hexadecimal digits from a byte array. Each
     * byte is converted to 2 hex symbols.
     */
    private static String toString(byte[] ba) {
        int length = ba.length;
        char[] buf = new char[length * 2];

        for (int i = 0, j = 0, k; i < length;) {
            k = ba[i++];
            buf[j++] = HEX_DIGITS[(k >>> 4) & 0x0F];
            buf[j++] = HEX_DIGITS[k & 0x0F];
        }

        return new String(buf);
    }

    /**
     * Returns a string of hexadecimal digits from an integer array. Each
     * int is converted to 4 hex symbols.
     */
    private static String toString(int[] ia) {
        int length = ia.length;
        char[] buf = new char[length * 8];

        for (int i = 0, j = 0, k; i < length; i++) {
            k = ia[i];
            buf[j++] = HEX_DIGITS[(k >>> 28) & 0x0F];
            buf[j++] = HEX_DIGITS[(k >>> 24) & 0x0F];
            buf[j++] = HEX_DIGITS[(k >>> 20) & 0x0F];
            buf[j++] = HEX_DIGITS[(k >>> 16) & 0x0F];
            buf[j++] = HEX_DIGITS[(k >>> 12) & 0x0F];
            buf[j++] = HEX_DIGITS[(k >>> 8) & 0x0F];
            buf[j++] = HEX_DIGITS[(k >>> 4) & 0x0F];
            buf[j++] = HEX_DIGITS[k & 0x0F];
        }

        return new String(buf);
    }
}
